﻿Public NotInheritable Class ConsoleInput

    Private Shared _ResponsePrompt As String = CommandInterpreter.DefaultCommandPrompt
    ''' <summary>
    ''' Gets or sets the prompt string used when prompting the user for input. Uses CommandInterpreter.DefaultCommandPrompt by default.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Property ResponsePrompt As String
        Get
            Return _ResponsePrompt
        End Get
        Set(value As String)
            If value Is Nothing Then value = String.Empty
            _ResponsePrompt = value
        End Set
    End Property

    Public Shared Property MenuItemColor As ConsoleColor = ConsoleColor.White
    Public Shared Property MenuTitleColor As ConsoleColor = ConsoleColor.Green
    Public Shared Property MenuNumberColor As ConsoleColor = ConsoleColor.Yellow
    Public Shared Property MenuNumberSuffix As String = ">"
    Public Shared Property MenuNumberSuffixColor As ConsoleColor = ConsoleColor.DarkYellow

    Public Shared Property MessageIndent As Integer = 2
    Public Shared Property YesOrNoMessage As String = "(\[c14](Y)\[c16]es or \[c14](N)\[c16]o)"
    Public Shared Property YesNoMessageCharacterYes As ConsoleKey = ConsoleKey.Y
    Public Shared Property YesNoMessageCharacterNo As ConsoleKey = ConsoleKey.N

    Protected Sub New()
    End Sub

    ''' <summary>
    ''' Displays a menu to the user, forcing them to make a choice from the list in order to continue.
    ''' </summary>
    ''' <param name="menuTitle">The title to display at the top of the menu.</param>
    ''' <param name="menuItems">The list of strings used to generate the menu item list.</param>
    ''' <returns>A ConsoleInputMenuSelection instance representing the menu item selected by the user, or Empty if the menu failed to display.</returns>
    ''' <remarks>Displays an organized menu centered on screen with the supplied title at the top, followed by a blank line,
    ''' followed by the supplied menu items, numbered, and placed in columns as required to fit.  If there are too
    ''' many items (or too many long items) a message will be written and an empty ConsoleInputMenuSelection object
    ''' will be returned.</remarks>
    Public Shared Function GetMenuSelection(menuTitle As String, menuItems As IEnumerable(Of String)) As ConsoleInputMenuSelection
        Console.Clear()
        If menuItems.Count = 0 Then
            ConsoleOutput.WriteIndentedColored(3, ConsoleColor.Red, True, "MENU EMPTY")
            Return ConsoleInputMenuSelection.Empty
        End If
        If menuTitle Is Nothing Then menuTitle = String.Empty
        Dim deltaHeight As Integer = Console.WindowHeight - 4
        Dim columnCount As Integer = CInt(Math.Ceiling(menuItems.Count / deltaHeight))
        Dim maxItemLength As Integer = menuItems.Max(Function(s As String) s.Length)
        Dim lineWidth As Integer = (maxItemLength + 4) * columnCount
        If lineWidth <= Console.WindowWidth Then
            Dim menuBuilder As New System.Text.StringBuilder
            Dim deltaX As Integer
            deltaX = (Console.WindowWidth / 2) - (menuTitle.Length / 2)
            menuBuilder.Append(ConsoleOutput.GetCursorRightFormatCode(deltaX))
            menuBuilder.Append(ConsoleOutput.GetColorFormatCode(MenuTitleColor))
            menuBuilder.Append(menuTitle)
            menuBuilder.Append("\[c16]")
            menuBuilder.Append(ConsoleOutput.GetNewLineFormatCode(2))
            deltaX = (Console.WindowWidth / 2) - (lineWidth / 2)
            Dim index As Integer = 0
            Dim selections As New List(Of ConsoleInputMenuSelection)
            Dim addedIndexes As New List(Of Integer)
            While index < menuItems.Count
                If Not addedIndexes.Contains(index) Then
                    menuBuilder.Append(ConsoleOutput.GetCursorRightFormatCode(deltaX))
                    menuBuilder.AppendFormat("\[c{1}]{0:d2}\[c{2}]{3}\[c16] ", index + 1, CInt(MenuNumberColor), CInt(MenuNumberSuffixColor), MenuNumberSuffix)
                    menuBuilder.AppendFormat("\[c{1}]{0}\[c16]", menuItems(index), CInt(MenuItemColor))
                    selections.Add(New ConsoleInputMenuSelection(menuItems(index), index + 1, index))
                    addedIndexes.Add(index)
                    Dim lastLength As Integer = menuItems(index).Length
                    Dim testIndex As Integer = index
                    For count As Integer = 0 To columnCount - 1
                        testIndex += deltaHeight
                        If menuItems.Count > testIndex Then
                            menuBuilder.AppendFormat("{4}\[c{1}]{0:d2}\[c{2}]{3}\[c16] ", testIndex + 1, CInt(MenuNumberColor), CInt(MenuNumberSuffixColor), MenuNumberSuffix, Space(maxItemLength - lastLength + 1))
                            menuBuilder.AppendFormat("\[c{1}]{0}\[c16]", menuItems(testIndex), CInt(MenuItemColor))
                            lastLength = menuItems(testIndex).Length
                            selections.Add(New ConsoleInputMenuSelection(menuItems(testIndex), testIndex + 1, testIndex))
                            addedIndexes.Add(testIndex)
                        End If
                    Next
                    menuBuilder.Append("\[n]")
                End If
                index += 1
            End While
            ConsoleOutput.WriteFormatted(menuBuilder.ToString)
            Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent
            ConsoleOutput.Write(ResponsePrompt)
            Dim result As ConsoleInputMenuSelection = ConsoleInputMenuSelection.Empty
            While result.MenuNumber = 0
                Dim userInput As String = Console.ReadLine.Trim
                Dim menuNumber As Integer
                If Integer.TryParse(userInput, menuNumber) Then
                    result = selections.Find(Function(item As ConsoleInputMenuSelection) item.MenuNumber = menuNumber)
                End If
                Console.CursorTop -= 1
                Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
                Console.Write(Space(Console.WindowWidth - Console.CursorLeft - 1))
                Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
            End While
            Console.CursorLeft = 0
            Console.Write(Space(Console.WindowWidth - 1))
            Console.CursorLeft = 0
            Return result
        Else
            ConsoleOutput.WriteIndentedColored(3, ConsoleColor.Red, True, "MENU TOO LONG")
            Return ConsoleInputMenuSelection.Empty
        End If
    End Function

    ''' <summary>
    ''' Prompts the user to enter text and waits for a valid response.
    ''' </summary>
    ''' <param name="prompt">The prompt to display to the user.</param>
    ''' <returns>The string text entered by the user.</returns>
    ''' <remarks></remarks>
    Public Shared Function GetTextResponse(prompt As String) As String
        If prompt IsNot Nothing AndAlso prompt.Length > 0 Then
            ConsoleOutput.WriteFormatted(ConsoleOutput.GetCursorRightFormatCode(CommandInterpreter.ResultIndentation) & prompt)
            ConsoleOutput.WriteLine()
        End If
        Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent
        ConsoleOutput.Write(ResponsePrompt)
        Dim result As String = String.Empty
        While result.Length = 0
            result = Console.ReadLine.Trim
            Console.CursorTop -= 1
            Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
        End While
        Console.CursorLeft = 0
        Console.Write(Space(Console.WindowWidth - 1))
        Console.CursorLeft = 0
        Return result
    End Function

    ''' <summary>
    ''' Prompts the user to enter a decimal number and waits for a valid response. All invalid entries are ignored.
    ''' </summary>
    ''' <param name="prompt">The prompt to display to the user.</param>
    ''' <returns>The decimal number entered by the user.</returns>
    ''' <remarks></remarks>
    Public Shared Function GetDecimalResponse(prompt As String) As Decimal
        If prompt IsNot Nothing AndAlso prompt.Length > 0 Then
            ConsoleOutput.WriteFormatted(ConsoleOutput.GetCursorRightFormatCode(CommandInterpreter.ResultIndentation) & prompt)
            ConsoleOutput.WriteLine()
        End If
        Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent
        ConsoleOutput.Write(ResponsePrompt)
        Dim result As Nullable(Of Decimal) = Nothing
        While result Is Nothing
            Dim userInput As String = Console.ReadLine.Trim
            If Decimal.TryParse(userInput, Nothing) Then
                result = CDec(userInput)
            End If
            Console.CursorTop -= 1
            Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
            Console.Write(Space(Console.WindowWidth - Console.CursorLeft - 1))
            Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
        End While
        Console.CursorLeft = 0
        Console.Write(Space(Console.WindowWidth - 1))
        Console.CursorLeft = 0
        Return result
    End Function

    ''' <summary>
    ''' Prompts the user to enter an integer number and waits for a valid response. All invalid entries are ignored.
    ''' </summary>
    ''' <param name="prompt">The prompt to display to the user.</param>
    ''' <returns>The integer number entered by the user.</returns>
    ''' <remarks></remarks>
    Public Shared Function GetIntegerResponse(prompt As String) As Integer
        If prompt IsNot Nothing AndAlso prompt.Length > 0 Then
            ConsoleOutput.WriteFormatted(ConsoleOutput.GetCursorRightFormatCode(CommandInterpreter.ResultIndentation) & prompt)
            ConsoleOutput.WriteLine()
        End If
        Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent
        ConsoleOutput.Write(ResponsePrompt)
        Dim result As Nullable(Of Integer) = Nothing
        While result Is Nothing
            Dim userInput As String = Console.ReadLine.Trim
            If Integer.TryParse(userInput, Nothing) Then
                result = CInt(userInput)
            End If
            Console.CursorTop -= 1
            Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
            Console.Write(Space(Console.WindowWidth - Console.CursorLeft - 1))
            Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
        End While
        Console.CursorLeft = 0
        Console.Write(Space(Console.WindowWidth - 1))
        Console.CursorLeft = 0
        Return result
    End Function

    ''' <summary>
    ''' Prompts the user to enter a date and waits for a valid response. All invalid entries are ignored.
    ''' </summary>
    ''' <param name="prompt">The prompt to display to the user.</param>
    ''' <returns>The date entered by the user.</returns>
    ''' <remarks></remarks>
    Public Shared Function GetDateResponse(prompt As String) As Date
        If prompt IsNot Nothing AndAlso prompt.Length > 0 Then
            ConsoleOutput.WriteFormatted(ConsoleOutput.GetCursorRightFormatCode(CommandInterpreter.ResultIndentation) & prompt)
            ConsoleOutput.WriteLine()
        End If
        Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent
        ConsoleOutput.Write(ResponsePrompt)
        Dim result As Nullable(Of Date) = Nothing
        While result Is Nothing
            Dim userInput As String = Console.ReadLine.Trim
            If Date.TryParse(userInput, Nothing) Then
                result = CDate(userInput)
            End If
            Console.CursorTop -= 1
            Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
            Console.Write(Space(Console.WindowWidth - Console.CursorLeft - 1))
            Console.CursorLeft = CommandInterpreter.ResultIndentation + MessageIndent + ResponsePrompt.Length
        End While
        Console.CursorLeft = 0
        Console.Write(Space(Console.WindowWidth - 1))
        Console.CursorLeft = 0
        Return result
    End Function

    ''' <summary>
    ''' Prompts the user with a yes-or-no quesiton and waits for a valid response. All invalid entries are ignored.
    ''' </summary>
    ''' <param name="prompt">The prompt to display to the user.</param>
    ''' <returns>A boolean representing the user's response.</returns>
    ''' <remarks></remarks>
    Public Shared Function GetYesNoResponse(prompt As String) As Boolean
        If prompt IsNot Nothing AndAlso prompt.Length > 0 Then
            ConsoleOutput.WriteFormatted(ConsoleOutput.GetCursorRightFormatCode(CommandInterpreter.ResultIndentation) & prompt)
            ConsoleOutput.WriteLine()
        End If
        ConsoleOutput.WriteFormatted(ConsoleOutput.GetCursorRightFormatCode(CommandInterpreter.ResultIndentation + MessageIndent) & YesOrNoMessage)
        ConsoleOutput.WriteLine()
        Console.CursorLeft += CommandInterpreter.ResultIndentation + MessageIndent
        Dim testKey As ConsoleKey = ConsoleKey.NoName
        While Not (testKey = YesNoMessageCharacterYes OrElse testKey = YesNoMessageCharacterNo)
            testKey = Console.ReadKey(True).Key
        End While
        Return testKey = YesNoMessageCharacterYes
    End Function

End Class

Public Structure ConsoleInputMenuSelection
    ''' <summary>
    ''' Gets the text of the menu item.
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly MenuItemName As String
    ''' <summary>
    ''' Gets the menu number assigned to the item.
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly MenuNumber As Integer
    ''' <summary>
    ''' Gets the index of the item in the source name collection.
    ''' </summary>
    ''' <remarks></remarks>
    Public ReadOnly SourceIndex As Integer

    Public Shared ReadOnly Empty As New ConsoleInputMenuSelection

    Public Sub New(name As String, number As Integer, index As Integer)
        MenuItemName = name
        MenuNumber = number
        SourceIndex = index
    End Sub
End Structure